using System;
using System.Text;
using Auxiliary.VecMath;
using Tao.OpenGl;

namespace Auxiliary.OpenGL
{
	public sealed class ShaderProgram : IDisposable
	{
        private int _vertexShader = Gl.glCreateShader(Gl.GL_VERTEX_SHADER);
        private int _fragmentShader = Gl.glCreateShader(Gl.GL_FRAGMENT_SHADER);
        private int _program = Gl.glCreateProgram();
        private readonly StringBuilder _log = new StringBuilder();

        public ShaderProgram(string[] vertexShader, string[] fragmentShader)
        {
            _log.AppendLine("****************************************************");
            _log.AppendLine("* Setting vertex shader                            *");
            _log.AppendLine("****************************************************");

            SetShaderSource(_vertexShader, vertexShader);

            _log.AppendLine("****************************************************");
            _log.AppendLine("* Setting fragment shader                          *");
            _log.AppendLine("****************************************************");

            SetShaderSource(_fragmentShader, fragmentShader);

            _log.AppendLine("****************************************************");
            _log.AppendLine("* Linking program object                           *");
            _log.AppendLine("****************************************************");

            LinkProgram();
        }

        public void Dispose()
        {
            Gl.glDeleteShader(_vertexShader);
            _vertexShader = 0;

            Gl.glDeleteShader(_fragmentShader);
            _fragmentShader = 0;

            Gl.glDeleteProgram(_program);
            _program = 0;
        }

        public void Bind()
        {
            Gl.glUseProgram(_program);
        }

        public void UnBind()
        {
            Gl.glUseProgram(0);
        }

        public Vector4D GetUniformVector(string name)
        {
            var location = GetUniformLocation(name);

            if (location < 0)
                return Vector4D.Zero;

            var values = new float[4];
            Gl.glGetUniformfv(_program, location, values);

            return new Vector4D(values);
        }

        public bool SetUniform(string name, int value)
        {
            var location = GetUniformLocation(name);

            if (location < 0)
                return false;

            Gl.glUniform1i(location, value);

            return true;
        }

        public bool SetUniform(string name, float value)
        {
            var location = GetUniformLocation(name);

            if (location < 0)
                return false;

            Gl.glUniform1f(location, value);

            return true;
        }

        public bool SetUniform(string name, Vector2D value)
        {
            var location = GetUniformLocation(name);

            if (location < 0)
                return false;

            Gl.glUniform2fv(location, 1, value.ToArray());

            return true;
        }

        public bool SetUniform(string name, Vector3D value)
        {
            var location = GetUniformLocation(name);

            if (location < 0)
                return false;

            Gl.glUniform3fv(location, 1, value.ToArray());

            return true;
        }

        public bool SetUniform(string name, Vector4D value)
        {
            var location = GetUniformLocation(name);

            if (location < 0)
                return false;

            Gl.glUniform4fv(location, 1, value.ToArray());

            return true;
        }

        public bool SetUniform(string name, Matrix2D value)
        {
            var location = GetUniformLocation(name);

            if (location < 0)
                return false;

            Gl.glUniformMatrix2fv(location, 1, Gl.GL_TRUE, value.ToArray());

            return true;
        }
        
        public bool SetUniform(string name, Matrix3D value)
        {
            var location = GetUniformLocation(name);

            if (location < 0)
                return false;

            Gl.glUniformMatrix3fv(location, 1, Gl.GL_TRUE, value.ToArray());

            return true;
        }
        
        public bool SetUniform(string name, Matrix4D value)
        {
            var location = GetUniformLocation(name);

            if (location < 0)
                return false;

            Gl.glUniformMatrix4fv(location, 1, Gl.GL_TRUE, value.ToArray());

            return true;
        }

        public bool SetSampler(string name, int unit)
        {
            var location = GetUniformLocation(name);

            if (location < 0)
                return false;

            Gl.glUniform1i(location, unit);

            return true;
        }
        
        public bool SetAttribute(string name, float value)
        {
            var location = GetAttributeLocation(name);

            if (location < 0)
                return false;

            Gl.glVertexAttrib1f(location, value);

            return true;
        }
        
        public bool SetAttribute(string name, Vector2D value)
        {
            var location = GetAttributeLocation(name);

            if (location < 0)
                return false;

            Gl.glVertexAttrib2fv(location, value.ToArray());

            return true;
        }
      
        public bool SetAttribute(string name, Vector3D value)
        {
            var location = GetAttributeLocation(name);

            if (location < 0)
                return false;

            Gl.glVertexAttrib3fv(location, value.ToArray());

            return true;
        }
        
        public bool SetAttribute(string name, Vector4D value)
        {
            var location = GetAttributeLocation(name);

            if (location < 0)
                return false;

            Gl.glVertexAttrib4fv(location, value.ToArray());

            return true;
        }

	    public string Log
        {
            get
            {
                return _log.ToString();
            }
        }

        private void SetShaderSource(int shader, string[] lines)
        {
            _log.AppendLine("Loading shader source code");

            Gl.glShaderSource(shader, lines.Length, lines, null);

            _log.AppendLine("Compiling shader source code");

            Gl.glCompileShader(shader);
            
            var info = new StringBuilder();
            Gl.glGetShaderInfoLog(shader, Int32.MaxValue, null, info);

            _log.Append(info.ToString());

            int status;
            Gl.glGetShaderiv(shader, Gl.GL_COMPILE_STATUS, out status);

            if (0 == status)
            {
                _log.AppendLine("Error! Failed to load shader");

                throw new Exception("Failed to load shader");
            }

            _log.AppendLine("Attaching shader to program object");

            Gl.glAttachShader(_program, shader);
        }

        private void LinkProgram()
        {
            Gl.glLinkProgram(_program);

            var info = new StringBuilder();
            Gl.glGetProgramInfoLog(_program, Int32.MaxValue, null, info);
            _log.Append(info.ToString());

            int status;
            Gl.glGetProgramiv(_program, Gl.GL_LINK_STATUS, out status);

            if (0 == status)
            {
                _log.AppendLine("Error! Failed to link program");

                throw new Exception("Failed to link program");
            }
        }

        private int GetUniformLocation(string name)
        {
            return Gl.glGetUniformLocation(_program, name);
        }

        private int GetAttributeLocation(string name)
        {
            return Gl.glGetAttribLocation(_program, name);
        }
	}
}
